Appearance
视频地址: https://www.bilibili.com/video/BV1Q4411y7ip
参考文档: https://blog.csdn.net/hancoder/article/details/120213532
建议看一下旗下的视频课件文档
一、基础概念
CAP 理论
CAP理论是分布式计算中的一个重要理论,由Eric Brewer提出。CAP指的是Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性)。理论声称,一个分布式系统不可能同时满足这三个要求,最多只能同时满足其中两项:
- 一致性(C):所有节点在同一时间具有相同的数据。
- 可用性(A):保证每个请求都能得到一个响应,无论响应是成功还是失败。
- 分区容忍性(P):系统中任意信息的丢失或失败都不会影响系统的继续运作。
在实际应用中,分区容忍性是必须要保证的,因为网络分区在实际环境中是常见的(在分布式环境中,系统节点之间肯定的需要有网络连接的,因此分区(P) 是必然存在的)。因此,大多数分布式系统设计的抉择通常是在一致性和可用性之间做权衡。
BASE 理论
相对于CAP的严格要求,BASE理论提供了一种较为宽松的事务一致性模型。BASE是Basically Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致性)的缩写:
- 基本可用(Basically Available):分布式系统在出现故障的时候,允许损失部分可用性——例如,响应时间可能会延长。
- 软状态(Soft state):系统的状态不需要时刻一致,允许在不同节点间存在中间状态,而这种状态会随着时间的推移而逐渐一致。
- 最终一致性(Eventually consistent):系统保证在一定时间范围内,数据最终将是一致的。
BASE:牺牲强一致性,保证可用性,确保最终一致性。
BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终一致性。满足BASE理论的事务,我们称之为“柔性事务”。
分布式事务
数据库事务回顾:事务的特性是:ACID;
- 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
我们需要理解的是,什么情况下会出现分布式事务问题:
分布式事务产生的场景
- 1、典型的场景:微服务架构
- 微服务之间通过远程调用完成事务操作
- 跨 JVM 进程产生分布式事务问题
- 微服务之间通过远程调用完成事务操作
- 2、单体系统访问多个数据库实例
- 跨数据库实例产生分布式事务
- 3、多服务访问同一个数据库实例
- 跨 JVM 进程
- 两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。
二、分布式事务解决方案
在Java中处理分布式事务时,常见的方法有以下几种:
- 两阶段提交(2PC):是XA事务的一个实现,它分为“准备阶段”和“提交阶段”,确保所有参与者都同意提交事务。这种方式强调的是一致性,但牺牲了系统的可用性。
- 补偿事务(TCC,Try-Confirm-Cancel):这种方式首先执行试操作,如果所有参与者都成功,则进行确认操作,否则执行取消操作。它适合于业务逻辑较为复杂的系统。
- 最终一致性框架:如 Apache Kafka 和 RocketMQ 等消息中间件,通过异步消息确保最终一致性。这类方法强调的是可用性和分区容忍性。
- 分布式事务中间件:例如 Seata ,它通过创建分布式事务协调者来管理各个微服务之间的事务,实现分布式事务的一致性。
2PC 两阶段提交
- 两阶段:准备阶段 Prepare phase、提交阶段 comomit phase
数据库支持的2pc【二阶段提交】,又叫做 XA Transactions
2PC(Two-phase commit protocol),中文叫二阶段提交。 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。
XA:强一致性,比较适⽤于执⾏时间确定的短事务,整体性能比较差。
在计算机中部分关系数据库如Oracle、MySQL支持两阶段提交协议:
- 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。
- (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)
- 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
如果任一资源管理器在第一阶段返回准备失败,那么事务管理器会要求所有资源管理器在第二阶段执行回滚操作。通过事务管理器的两阶段协调,最终所有资源管理器要么全部提交,要么全部回滚,最终状态都是一致的
TCC
Seata
MQ
三、Seata 的使用示例
基础概念
文档: https://seata.apache.org/zh-cn/blog/seata-quick-start/
Seata 是阿里开源的一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务
事务模式

Seata 的角色
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围,开始全局事务、提交或回滚全局事务。
- RM ( Resource Manager ) - 资源管理器:管理分支事务处理的资源( Resource ),与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端

分布式事务的生命周期

- TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。
- XID,会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。
- RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
- TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
- TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。

环境准备
虚拟机安装一下 MySQL 5.7 、Nacos 1.3.2 版本
部署单机 TC Server
TC 需要进行全局事务和分支事务的记录,所以需要对应的存储。目前,TC 有两种存储模式( store.mode ):
- file 模式:适合单机模式,全局事务会话信息在内存中读写,并持久化本地文件
root.data,性能较高。 - db 模式:适合集群模式,全局事务会话信息通过 db 共享,相对性能差点。
这个不用管,写的有点问题 👆
Seata 服务
根据项目的介绍文档,这里下载 1.3 版本的 seata 的服务
这里先不用 docker 的方式,有点理解问题,先使用一下 windows 下的 zip 方式
下载地址: https://github.com/apache/incubator-seata/releases?page=2
window下载zip,linux/mac下载tar.gz

前期准备
这里就下载一下这个项目,并将数据库文件执行一下,windows 的相关操作先不管
看到一个很不错的项目,这里使用一下它:https://github.com/YunaiV/SpringBoot-Labs 的 labx-17 目录
seata + openfeign
改一下这个 pom 文件
<!-- <parent>-->
<!-- <artifactId>labx-17</artifactId>-->
<!-- <groupId>cn.iocoder.springboot.labs</groupId>-->
<!-- <version>1.0-SNAPSHOT</version>-->
<!-- </parent>-->
<groupId>cn.iocoder.springboot.labs</groupId>
<artifactId>labx-17-sc-seata-at-feign-demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>执行 data.sql 文件
部署过程看一下这个视频,挺详细的: https://www.bilibili.com/video/BV1wW4y147k3
改一下这两个文件的配置内容

file.conf
修改配置为通过 db 的方式,改一下 mysql 相关的配置文件内容

对应数据库中,执行以下脚本文件: https://github.com/apache/incubator-seata/blob/1.3.0/script/server/db/mysql.sql
先创建一下数据库

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;registry.conf
改一下配置中心和注册中心的配置内容
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.56.105:8848"
group = "SEATA_GROUP"
#不写默认是 public
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
...
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "192.168.56.105:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
...在 nacos 中添加配置文件

配置内容: https://github.com/apache/incubator-seata/blob/1.3.0/script/config-center/config.txt
微改了一下,后面有需要再进行改动
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default
# 端口 8091 通常用于 Seata 服务的默认通信端口
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=file
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.56.105:3306/seata?useUnicode=true
store.db.user=username
store.db.password=password
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898启动操作
直接点击 seata-server.bat 文件进行启动操作

查看服务实例,发现是已经在服务列表了

然后再启动一下客户端,启动前先过一下基础概念内容

启动客户端有报错信息
2024-04-27 23:03:46.363 ERROR 15912 --- [imeoutChecker_2] i.s.c.r.netty.NettyClientChannelManager : no available service 'default' found, please make sure registry config correct部署 服务端 TC
有问题,这里改一下为 docker 的方式进行部署,看一下是否是 TC 端的问题(看很多是可以进行一个 config.txt 文件推送的)
这里使用 docker 的方式进行部署
docker pull seataio/seata-server:1.3.0部署参考:
- https://seata.apache.org/zh-cn/docs/ops/deploy-by-docker/
- https://www.cnblogs.com/lvlinguang/p/17038658.html#1docker方式安装
新建目录
mkdir -p /home/apps/seata/config
# 进入目录
cd /home/apps/seata/confignacos 中新建一个 seata 的命名空间
新建 registry.conf
# 注册中心
registry {
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.56.105:8848"
group = "SEATA_GROUP"
namespace = "6487cc37-2434-4574-81f9-83d2c2fb0dd5"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
# 配置中心
config {
type = "nacos"
nacos {
serverAddr = "192.168.56.105:8848"
namespace = "6487cc37-2434-4574-81f9-83d2c2fb0dd5"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}启动命令
docker run \
-d \
--name seata-server \
--restart=always \
--privileged=true \
-p 8091:8091 \
-e SEATA_IP=192.168.56.105 \
-e SEATA_PORT=8091 \
-e SEATA_CONFIG_NAME=file:/root/seata-config/registry \
-v /home/apps/seata/config:/root/seata-config \
seataio/seata-server:1.3.0推送配置信息
下载地址: https://github.com/seata/seata/tree/1.3.0
目录: incubator-seata-1.3.0\script\config-center\config.txt
config.txt
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=file
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.56.105:3306/seata?useUnicode=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
推送操作,进入改目录
nacos-config.sh -h 192.168.56.105 -p 8848 -g SEATA_GROUP -t 6487cc37-2434-4574-81f9-83d2c2fb0dd5这个操作会推送对应配置到相应的命令空间区域
根据后面反馈情况,需要再改一下这个配置
15 行后面再加三行配置
transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default
service.vgroupMapping.account-service-group=default
service.vgroupMapping.order-service-group=default
service.vgroupMapping.product-service-group=default
service.default.grouplist=127.0.0.1:8091添加配置后,再进行一次推送(重复命令即可,看了一下他 set 执行的是覆盖操作)
修改配置文件,客户端启动
改一下配置文件,主要是命名空间,然后还有 tx-service-group 的相关配置
三个模块的内容都参考下面的改一下;另外还有就是 seata 的 版本统一改为了 1.3.0
server:
port: 8081 # 端口
spring:
application:
name: order-service
datasource:
url: jdbc:mysql://192.168.56.105:3306/seata_order?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
cloud:
# Nacos 作为注册中心的配置项
nacos:
discovery:
server-addr: 192.168.56.105:8848
namespace: 6487cc37-2434-4574-81f9-83d2c2fb0dd5
# group: SEATA_GROUP
seata:
enabled: true
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
rapid_cloud_tx_group: default
registry:
type: nacos
nacos:
application: seata-server
server-addr: 192.168.56.105:8848
group: SEATA_GROUP
namespace: '6487cc37-2434-4574-81f9-83d2c2fb0dd5'
username: 'nacos'
password: 'nacos'
config:
type: nacos
nacos:
server-addr: 192.168.56.105:8848
group: SEATA_GROUP
namespace: '6487cc37-2434-4574-81f9-83d2c2fb0dd5'
username: 'nacos'
password: 'nacos'这个配置再改一下
seata:
application-id: ${spring.application.name} # Seata 应用编号,默认为 ${spring.application.name}
tx-service-group: ${spring.application.name}-group
service:
vgroup-mapping:
rapid_cloud_tx_group: default
registry:
type: nacos
nacos:
application: seata-server
server-addr: 192.168.56.105:8848
group: SEATA_GROUP
namespace: '6487cc37-2434-4574-81f9-83d2c2fb0dd5'
username: 'nacos'
password: 'nacos'
config:
type: nacos
nacos:
server-addr: 192.168.56.105:8848
group: SEATA_GROUP
namespace: '6487cc37-2434-4574-81f9-83d2c2fb0dd5'
username: 'nacos'
password: 'nacos'后面看了下,再改一下
其中 product-service-group: default 是对应 tx-service-group 前面的 config.txt 中的分组映射关系
# Seata 配置项,对应 SeataProperties 类
seata:
application-id: ${spring.application.name} # Seata 应用编号,默认为 ${spring.application.name}
tx-service-group: ${spring.application.name}-group # Seata 事务组编号,用于 TC 集群名
# 服务配置项,对应 ServiceProperties 类
service:
# 虚拟组和分组的映射
vgroup-mapping:
product-service-group: default
# account-service-group: default
# order-service-group: default
# 分组和 Seata 服务的映射
grouplist:
#设置Seata TC 的地址
default: 192.168.56.105:8091部署集群 TC Server
这里后面再看一下 to be contined....
业务场景
继续项目:https://github.com/YunaiV/SpringBoot-Labs 的 labx-17
文档: https://www.iocoder.cn/Spring-Cloud-Alibaba/Seata/?self
业务逻辑

项目模块

- order 下单操作
- product 商品服务(扣除库存)
- account 账户服务(扣除余额)
data.sql 文件中: 每个库中的 undo_log 表,是 Seata AT 模式必须创建的表,主要用于分支事务的回滚
考虑到测试方便,数据库中插入了一条 id = 1 的 account 记录,和一条 id = 1 的 product 记录
简单测试
测试两种情况:
- 分布式事务正常提交
- 分布式事务异常回滚
分布式事务正常提交
如果要正常提交的话,因为账户表中只有一条数据,用户 1 有10 余额;产品表一条数据,产品 1 有10个库存

这个是一次正常请求记录

日志,记录正常操作:

分布式事务异常回滚
模仿余额不足的情况下

相关的日志

工程代码
看一下核心代码
@Override
@GlobalTransactional
public Integer createOrder(Long userId, Long productId, Integer price) {
Integer amount = 1; // 购买数量,暂时设置为 1。
logger.info("[createOrder] 当前 XID: {}", RootContext.getXID());
// 扣减库存
productService.reduceStock(new ProductReduceStockDTO().setProductId(productId).setAmount(amount));
// 扣减余额
accountService.reduceBalance(new AccountReduceBalanceDTO().setUserId(userId).setPrice(price));
// 保存订单
OrderDO order = new OrderDO().setUserId(userId).setProductId(productId).setPayAmount(amount * price);
orderDao.saveOrder(order);
logger.info("[createOrder] 保存订单: {}", order.getId());
// 返回订单编号
return order.getId();
}中间几个都是远程调用的操作
然后对方执行的是本地事务,比如你看一下扣除余额的操作
@Override
@Transactional // 开启新事物
public void reduceBalance(Long userId, Integer price) throws Exception {
logger.info("[reduceBalance] 当前 XID: {}", RootContext.getXID());
// 检查余额
checkBalance(userId, price);
logger.info("[reduceBalance] 开始扣减用户 {} 余额", userId);
// 扣除余额
int updateCount = accountDao.reduceBalance(price);
// 扣除成功
if (updateCount == 0) {
logger.warn("[reduceBalance] 扣除用户 {} 余额失败", userId);
throw new Exception("余额不足");
}
logger.info("[reduceBalance] 扣除用户 {} 余额成功", userId);
}